Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Encodable and Decodable support for choice elements #119

Merged
merged 35 commits into from
Jul 30, 2019

Conversation

jsbean
Copy link
Contributor

@jsbean jsbean commented Jul 27, 2019

Introduction

This PR introduces the XMLChoiceCodingKey protocol, which enables the encoding and decoding of union-type–like enums with associated values to and from XML choice elements.

Resolves #25.
Resolves #91.

Motivation

XML schemas support choice elements, which constrain their contents to a single instance of a member of a known set of types. Choice elements exhibit the properties of union types and can be represented in Swift as enums with associated values, wherein each case of the enum carries with it a single associated value that is one of the types representable.

An example of how such a type is implemented in Swift
enum IntOrString {
    case int(Int)
    case string(String)
}

There is currently no automatic synthesis of the Codable protocol requirements for enums with assocated types in today's Swift. As such, it is required to provide custom implementations of the init(from: Decoder) initializer and the encode(to: Encoder) method to conform to the Encodable and Decodable protocols, respectively.

When encoding to and decoding from JSON, a single-element keyed container is created that uses the enum case name as the single key.

An example of adding Codable conformance to such a type when working with JSON
extension IntOrString: Codable {
    enum CodingKeys: String, CodingKey { case int, string }
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        do {
            self = .int(try container.decode(Int.self, forKey: .int))
        } catch {
            self = .string(try container.decode(String.self, forKey: .string))
        }
    }
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        switch self {
        case let .int(value):
            try container.encode(value, forKey: .int)
        case let .string(value):
            try container.encode(value, forKey: .string)
        }
    }
}

This may not be the most handsome approach, but it does the job without imprinting any format-specfic logic onto otherwise format-agnostic types.

This pattern works out of the box with the JSONEncoder and JSONDecoder provided by the Foundation framework. However, due to syntactic characteristics of the XML format, this pattern will not work automatically for encoding and decoding XML-formatted data, regardless of the tool used.

Proposed solution

The proposed solution is to define a new XMLChoiceCodingKey protocol:

/// An empty marker protocol that can be used in place of `CodingKey`.
/// It must be used when conforming a union-type–like enum with associated values to `Codable`
/// when the encoded format is `XML`.
public protocol XMLChoiceCodingKey: CodingKey {}

The XMLChoiceCodingKey protocol inherits from CodingKey and adds no new requirements. This conformance can be made retroactively, without additional implementation.

An example usage
extension IntOrString.CodingKeys: XMLChoiceCodingKey {}

Detailed design

This proposal adds a single public protocol XMLChoiceCodingKey, as well as several internal types.

Under the hood, the XMLChoiceEncodingContainer and XMLChoiceDecodingContainer are used to provide encode and decode methods tuned for XML choice elements.

Because of the characteristics of the XML format, there are some ambiguities (from an encoding and decoding perpsective) between unkeyed container elements that contain choice elements and those that contain nested unkeyed container elements.

In order to untangle these ambiguities, the new container types utilize a couple of new Box types to redirect elements along the encoding and decoding process.

Source compatibility

This is purely an additive change.

bwetherfield and others added 15 commits July 26, 2019 23:36
Add benchmark baselines

Pull in tests (#11)

Add Decoding support for choice elements (#15)

Fix indentation (#16)

Replace usage of XCTUnrwap (#19)

Add nested choice tests (#18)

Add falsely passing double array roundtrip test (#17)
Add CodingKey conformance to ChoiceKey
Add SingleElementEncodingContainer

Fix def

Expand ReferencingEncoder

Complete SingleElementEncodingContainer

Add decode-side hacks

Add special diversion for SingleElementContainer

Add hacky runtime bifurcation

Add case for XMLEncoder

Add SingleElementBox based XMLCoderElement initializer

Change open permissions to public

Add usage

Add usage

Add Nested cases within UnkeyedContainer

Make use of SingleElementBox key

Add UnkeyedBox of SingleElementBox fix

Add static check for array of SingleElementBox type

Implement nesting for keyed container

Fix top level container func

Implement nesting for SingleElementKeyed Container

Clean up SingleElementBox initialization

Fix formatting
Create interface for branching off XMLChoiceKeys

Add XMLSingleElementDecodingContainer copy pasta

Add XMLDecoderImplementation.singleElementContainer(keyedBy:) copy pasta

Push XMLChoiceKeys through singleElementContainer

Add XMLUnkeyedDecodingContainer.nestedSingleElementContainer copy pasta

Add XMLKeyedDecodingContainer.nestedSingleElementContainer copy pasta

Add XMLSingleElementDecodingContainer.nestedSingleElementContainer copy pasta

Remove print statement from test

Make IntOrStringWrapper.CodingKeys: XMLChoiceKey

Make Entry.CodingKeys: XMLChoiceKey

Only allow KeyedBoxes pass through to SingleElementDecodingContainer

Actually use XMLSingleElementDecodingContainer

Make tests pass

Rename XMLSingleElementDecodingContainer -> XMLChoiceDecodingContainer

Use ChoiceBox

Get rid of some prints

Unimplement singleElementContainer

Unimplement singleElementContainer

Tidy xcscheme

Unimplement nestedSingleElementContainer

Remove dump

Replace fatalError with thrown error

Omit type label

Omit type label

Fix formatting
Rename singleElementBox -> choiceBox

Create privileged path for Choices

Sweep away commented-out code

Add comment

Don't treat top-level choice

Tighten up impl

Rename singleElementContainer method -> choiceContainer

Whoops that was the Encoder

Add unkeyed single element container et al.

Add messages to fatal errors

Omit type label

Switch to ChoiceBox based implementation

Revert pretty printing special casing

Add passing encode tests for choice elements with attributes

Add xcodeproj debris

Remove use of XMLUnkeyedSingleElementDecodingContainer

Remove unreached code in XMLChoiceDecodingContainer

Remove superDecoder methods because enums ain't classes

Put all the decode impl in one place

Whitespace
Add doc comment for XMLChoiceCodingKey
Remove unused SingleKeyedBox.init?(_: KeyedBox)

Change internal property name from singleElementBox to singleKeyedBox

Remove internal property names singleElement -> singleKeyed
Rename header XMLSingleElementEncodingContainer -> XMLChoiceEncodingContainer

Fix formatting
Slim down SingleKeyedBox

Remove Foundation imports where not necessary
Add nested array encoding test

Add array within dictionary test

Formatting

Add keyed within unkeyed test

Add roundtrip test for keyed within keyed case

Update test to roundtrip for robustness

Add wrapped tests for encoding

Fix formatting
Resolve merge conflict

Replace xcodeproj
Reorder key decoding strategy cases

Touch less

Mutate directly
@MaxDesiatov MaxDesiatov self-assigned this Jul 27, 2019
@jsbean
Copy link
Contributor Author

jsbean commented Jul 28, 2019

Ah, it looks like we lost some xcodeproj luggage. Will pull that back in.

Copy link
Collaborator

@MaxDesiatov MaxDesiatov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great work @jsbean! I especially like the proposal format 👏

A few nitpicky requests here, but overall good to merge in my opinion as soon as those are resolved.

Sources/XMLCoder/Auxiliaries/Box/ChoiceBox.swift Outdated Show resolved Hide resolved
Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift Outdated Show resolved Hide resolved
Sources/XMLCoder/Encoder/XMLChoiceEncodingContainer.swift Outdated Show resolved Hide resolved
Sources/XMLCoder/Encoder/XMLChoiceEncodingContainer.swift Outdated Show resolved Hide resolved
topContainer = storage.pushChoiceContainer()
} else {
guard let container = storage.lastContainer as? SharedBox<ChoiceBox> else {
preconditionFailure("Attempt to push new (single element) keyed encoding container when already previously encoded at this path.")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this string be split into multiple lines with """ to bring it closer to 80 characters, or at least 100?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, this looks like a copy-the-style-as-it-is job. Do you want me to tidy up the other instances as well or just stick to the new code?

Copy link
Collaborator

@MaxDesiatov MaxDesiatov Jul 30, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes please, the overall cleanup is much appreciated!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a strict line length for the repo? The .swiftlint.yml declares violations at 150 and 200. I've been sticking to 100 out of habit, but I can constrain these to something else if it is so desired.

Copy link
Collaborator

@MaxDesiatov MaxDesiatov Jul 30, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been trying to get some balance between changing the whole repository and maintaining readability. 80 doesn't work well when indentation is 4 spaces instead of 2, but I decided to stick with 4 to preserve the changes history. You can clean up as much as you don't find tedious. I think last time I tried to tighten the limit in .swiftlint.yml (even to 120) it required too many code changes and reformatting XML snippets in the unit tests, which I just decided to postpone, but never got back to it. Maybe if there too many changes of this type, it can be made in a separate PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All seems fair to me! Probably worth waiting for the official swift-format to roll out to act on anything?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, makes sense

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, I just touched the long preconditionFailure messages.

Sources/XMLCoder/Auxiliaries/XMLChoiceCodingKey.swift Outdated Show resolved Hide resolved
Copy link
Collaborator

@MaxDesiatov MaxDesiatov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more thing if you don't mind: can you add a subsection to README.md in the advanced features section with a short description of the feature specifying that this is available since version 0.8.0, with the link to this PR there as well please? The new README section would contain a simple explanation of the feature, while the link to the PR would make the proposal itself visible for anyone curious.

@jsbean
Copy link
Contributor Author

jsbean commented Jul 30, 2019

A couple things, @MaxDesiatov.

  • In the XMLChoiceDecodingContainer, we currently fatalError if you attempt to do things which don't make sense (e.g., attempt to get a super container for an enum, attempt to get a nested container, etc.). Would you rather throw an error?
  • Do you want me to add anything to the README explaining this feature, or do we want to keep it on the low for now?

@MaxDesiatov
Copy link
Collaborator

MaxDesiatov commented Jul 30, 2019

@jsbean throwing an error is preferrable, since the error case can be unit-tested later if/when we want to increase the code coverage. As long as this doesn't add throws annotation to any public API that doesn't have it at the moment, otherwise need to think whether it worth making that breaking change.

@jsbean
Copy link
Contributor Author

jsbean commented Jul 30, 2019

Let me know if you'd like the README subsection to be more in-depth.

@jsbean
Copy link
Contributor Author

jsbean commented Jul 30, 2019

Before this makes it in, 👏 to @bwetherfield for the insight to cut this problem at the CodingKey level. It revealed an elegant symmetry for the implementation, and transformed the solution from a leak-plugging one to an architectural one.

@jsbean
Copy link
Contributor Author

jsbean commented Jul 30, 2019

(It seems that Homebrew is having a hard time only on the lint test on Azure.) Is there a way to re-run a test on that platform?

@MaxDesiatov
Copy link
Collaborator

CI passes now, thank you @jsbean and @bwetherfield! I plan to tag 0.8.0 with this feature and Linux support in the next few days.

@MaxDesiatov MaxDesiatov merged commit ab9fef0 into CoreOffice:master Jul 30, 2019
MaxDesiatov pushed a commit that referenced this pull request Jul 31, 2019
## Introduction

In merging in #119, we fixed most but not quite all of #91! Decoding of _null_ choice elements (represented as enums with empty struct associated values on the Swift side) still results in errors. This PR adds a test to demonstrate and fixes this issue by wrapping each `NullBox` inside of a `SingleKeyedBox` at the `merge` phase (called by `transformToBoxTree`).

## Motivation

One of the main lessons from #119 was that we have to wrap choice elements in the decoding phase to hold onto their keys. The keys are needed for directing us to the correct branch of the do-catch pyramid used for decoding. 

```swift
private enum Entry: Equatable {
    case run(Run)
    case properties(Properties)
    case br(Break)
}

extension Entry: Decodable {
    private enum CodingKeys: String, XMLChoiceCodingKey {
        case run, properties, br
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        do {
            self = .run(try container.decode(Run.self, forKey: .run))
        } catch {
            do {
                self = .properties(try container.decode(Properties.self, forKey: .properties))
            } catch {
                self = .br(try container.decode(Break.self, forKey: .br))
            }
        }
    }
}
```
where one of the associated values could be an empty struct (represented by null):

```swift
private struct Break: Decodable {}
```

Although we _can_ throw out keys for non-choice null elements, a mechanism is needed for holding onto the keys while transforming from the `XMLCoderElement` tree to the `boxTree`. Only later will we know if the key is needed (if this wrapped element is transformed to a `ChoiceBox`); if not, we will be able to throw out the key. 

## Proposed solution

The Public API is unchanged. On the implementation side, we catch `NullBox` values in `merge` and wrap them in `SingleKeyedBox` instances. 

## Detailed Design

In `merge`, we wrap each `NullBox` in a `SingleKeyedBox` with the appropriate key bundled in. An `XMLChoiceDecodingContainer` can be constructed from the `SingleKeyedBox` by converting it to a `ChoiceBox` (just transferring over the contents) - as normal. In `XMLKeyedDecodingContainer`, when preparing the `elements` for concrete decoding, we unwrap all `SingleKeyedBox` values that may be contained therein, as any choice elements contained would have already been transformed to a `ChoiceBox` by this point in decoding: any stray `SingleKeyedBox` wrappers can thus be thrown out. 

## Source compatibility

This is purely an additive change.
@jsbean jsbean deleted the choice-implementation branch September 30, 2019 22:39
MaxDesiatov pushed a commit that referenced this pull request Nov 27, 2019
## Overview

Fixes bug encountered when encoding structs that hold a mixture of choice-element and non-choice-element (or multiple choice-element) properties.

## Example

Given a structure that stores both a choice and non-choice property,
```swift
private struct IntOrStringAndDouble: Equatable {
    let intOrString: IntOrString
    let decimal: Double
}
```

the natural encoding approach (now available) is 
```swift
extension IntOrStringAndDouble: Encodable {
    enum CodingKeys: String, CodingKey {
        case decimal
    }

    func encode(to encoder: Encoder) {
        try intOrString.encode(to: encoder)
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(decimal, forKey: .decimal)
    }
}
```

The following `encode` implementation also works:

```swift
extension IntOrStringAndDouble: Encodable {
    enum CodingKeys: String, CodingKey {
        case decimal
    }

    func encode(to encoder: Encoder) {
        var singleValueContainer = encoder.singleValueContainer()
        try singleValueContainer.encode(intOrString)
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(decimal, forKey: .decimal)
    }
}
```

`IntOrString` as defined in #119:

```swift 
enum IntOrString: Equatable {
    case int(Int)
    case string(String)
}

extension IntOrString: Encodable {
    enum CodingKeys: String, CodingKey {
        case int
        case string
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        switch self {
        case let .int(value):
            try container.encode(value, forKey: .int)
        case let .string(value):
            try container.encode(value, forKey: .string)
        }
    }
}

extension IntOrString.CodingKeys: XMLChoiceCodingKey {} // signifies that `IntOrString` is a choice element
```

## Implementation Details

In cases where choice and non-choice elements (or multiple choice elements) co-exist in a keyed container, we merge them into a single `XMLKeyedEncodingContainer` (wrapping a `SharedBox<KeyedBox>`). Arrays of choice elements (using `XMLUnkeyedEncodingContainer` under the hood) are encoded the same way as before, as we do not hit the merging cases. For the array case, we still need the `XMLChoiceEncodingContainer` structure.

## Source Compatibility

This is an additive change.

* Add breaking case
* Add choice and keyed merging encode functionality
* Refactor
* Fix commented code
* Fix misnamed file
* Fix xcode project
* Fix precondition catch
* Use switch syntax
* Add multiple choice element case
* Add explicit types in KeyedBox initialization
* Add explicitly empty parameter to KeyedBox initializer
* Use more concise type inference
* Unify switch syntax
* Cut down code duplication
* Fix formatting
arjungupta0107 pushed a commit to salido/XMLCoder that referenced this pull request Jun 26, 2020
## Introduction

This PR introduces the `XMLChoiceCodingKey` protocol, which enables the encoding and decoding of union-type–like enums with associated values to and from `XML` choice elements.

Resolves CoreOffice#25.
Resolves CoreOffice#91.

## Motivation

XML schemas support [choice](https://www.w3schools.com/xml/el_choice.asp) elements, which constrain their contents to a single instance of a member of a known set of types. Choice elements exhibit the properties of [union types](https://en.wikipedia.org/wiki/Union_type) and can be represented in Swift as enums with associated values, wherein each case of the enum carries with it a single associated value that is one of the types representable.

An example of how such a type is implemented in Swift:
   
```Swift
enum IntOrString {
    case int(Int)
    case string(String)
}
```

There is currently no automatic synthesis of the `Codable` protocol requirements for enums with assocated types in today's Swift. As such, it is required to provide custom implementations of the `init(from: Decoder)` initializer and the `encode(to: Encoder)` method to conform to the `Encodable` and `Decodable` protocols, respectively.

When encoding to and decoding from `JSON`, a single-element keyed container is created that uses the enum case name as the single key.

An example of adding Codable conformance to such a type when working with JSON
   
```Swift
extension IntOrString: Codable {
    enum CodingKeys: String, CodingKey { case int, string }
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        do {
            self = .int(try container.decode(Int.self, forKey: .int))
        } catch {
            self = .string(try container.decode(String.self, forKey: .string))
        }
    }
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        switch self {
        case let .int(value):
            try container.encode(value, forKey: .int)
        case let .string(value):
            try container.encode(value, forKey: .string)
        }
    }
}
```

This may not be the most handsome approach, but it does the job without imprinting any format-specfic logic onto otherwise format-agnostic types.

This pattern works out of the box with the `JSONEncoder` and `JSONDecoder` provided by the `Foundation` framework. However, due to syntactic characteristics of the `XML` format, this pattern will **_not_** work automatically for encoding and decoding `XML`-formatted data, regardless of the tool used.

## Proposed solution

The proposed solution is to define a new `XMLChoiceCodingKey` protocol:

```Swift
/// An empty marker protocol that can be used in place of `CodingKey`.
/// It must be used when conforming a union-type–like enum with associated values to `Codable`
/// when the encoded format is `XML`.
public protocol XMLChoiceCodingKey: CodingKey {}
```

The `XMLChoiceCodingKey` protocol inherits from `CodingKey` and adds no new requirements. This conformance can be made retroactively, without additional implementation.

An example usage:
    
```Swift
extension IntOrString.CodingKeys: XMLChoiceCodingKey {}
``` 

## Detailed design

This proposal adds a single `public` `protocol` `XMLChoiceCodingKey`, as well as several `internal` types.

Under the hood, the `XMLChoiceEncodingContainer` and `XMLChoiceDecodingContainer` are used to provide `encode` and `decode` methods tuned for `XML` choice elements.

Because of the characteristics of the `XML` format, there are some ambiguities (from an encoding and decoding perpsective) between unkeyed container elements that contain choice elements and those that contain nested unkeyed container elements.

In order to untangle these ambiguities, the new container types utilize a couple of new `Box` types to redirect elements along the encoding and decoding process.

## Source compatibility

This is purely an additive change.
arjungupta0107 pushed a commit to salido/XMLCoder that referenced this pull request Jun 26, 2020
## Introduction

In merging in CoreOffice#119, we fixed most but not quite all of CoreOffice#91! Decoding of _null_ choice elements (represented as enums with empty struct associated values on the Swift side) still results in errors. This PR adds a test to demonstrate and fixes this issue by wrapping each `NullBox` inside of a `SingleKeyedBox` at the `merge` phase (called by `transformToBoxTree`).

## Motivation

One of the main lessons from CoreOffice#119 was that we have to wrap choice elements in the decoding phase to hold onto their keys. The keys are needed for directing us to the correct branch of the do-catch pyramid used for decoding. 

```swift
private enum Entry: Equatable {
    case run(Run)
    case properties(Properties)
    case br(Break)
}

extension Entry: Decodable {
    private enum CodingKeys: String, XMLChoiceCodingKey {
        case run, properties, br
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        do {
            self = .run(try container.decode(Run.self, forKey: .run))
        } catch {
            do {
                self = .properties(try container.decode(Properties.self, forKey: .properties))
            } catch {
                self = .br(try container.decode(Break.self, forKey: .br))
            }
        }
    }
}
```
where one of the associated values could be an empty struct (represented by null):

```swift
private struct Break: Decodable {}
```

Although we _can_ throw out keys for non-choice null elements, a mechanism is needed for holding onto the keys while transforming from the `XMLCoderElement` tree to the `boxTree`. Only later will we know if the key is needed (if this wrapped element is transformed to a `ChoiceBox`); if not, we will be able to throw out the key. 

## Proposed solution

The Public API is unchanged. On the implementation side, we catch `NullBox` values in `merge` and wrap them in `SingleKeyedBox` instances. 

## Detailed Design

In `merge`, we wrap each `NullBox` in a `SingleKeyedBox` with the appropriate key bundled in. An `XMLChoiceDecodingContainer` can be constructed from the `SingleKeyedBox` by converting it to a `ChoiceBox` (just transferring over the contents) - as normal. In `XMLKeyedDecodingContainer`, when preparing the `elements` for concrete decoding, we unwrap all `SingleKeyedBox` values that may be contained therein, as any choice elements contained would have already been transformed to a `ChoiceBox` by this point in decoding: any stray `SingleKeyedBox` wrappers can thus be thrown out. 

## Source compatibility

This is purely an additive change.
arjungupta0107 pushed a commit to salido/XMLCoder that referenced this pull request Jun 26, 2020
## Overview

Fixes bug encountered when encoding structs that hold a mixture of choice-element and non-choice-element (or multiple choice-element) properties.

## Example

Given a structure that stores both a choice and non-choice property,
```swift
private struct IntOrStringAndDouble: Equatable {
    let intOrString: IntOrString
    let decimal: Double
}
```

the natural encoding approach (now available) is 
```swift
extension IntOrStringAndDouble: Encodable {
    enum CodingKeys: String, CodingKey {
        case decimal
    }

    func encode(to encoder: Encoder) {
        try intOrString.encode(to: encoder)
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(decimal, forKey: .decimal)
    }
}
```

The following `encode` implementation also works:

```swift
extension IntOrStringAndDouble: Encodable {
    enum CodingKeys: String, CodingKey {
        case decimal
    }

    func encode(to encoder: Encoder) {
        var singleValueContainer = encoder.singleValueContainer()
        try singleValueContainer.encode(intOrString)
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(decimal, forKey: .decimal)
    }
}
```

`IntOrString` as defined in CoreOffice#119:

```swift 
enum IntOrString: Equatable {
    case int(Int)
    case string(String)
}

extension IntOrString: Encodable {
    enum CodingKeys: String, CodingKey {
        case int
        case string
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        switch self {
        case let .int(value):
            try container.encode(value, forKey: .int)
        case let .string(value):
            try container.encode(value, forKey: .string)
        }
    }
}

extension IntOrString.CodingKeys: XMLChoiceCodingKey {} // signifies that `IntOrString` is a choice element
```

## Implementation Details

In cases where choice and non-choice elements (or multiple choice elements) co-exist in a keyed container, we merge them into a single `XMLKeyedEncodingContainer` (wrapping a `SharedBox<KeyedBox>`). Arrays of choice elements (using `XMLUnkeyedEncodingContainer` under the hood) are encoded the same way as before, as we do not hit the merging cases. For the array case, we still need the `XMLChoiceEncodingContainer` structure.

## Source Compatibility

This is an additive change.

* Add breaking case
* Add choice and keyed merging encode functionality
* Refactor
* Fix commented code
* Fix misnamed file
* Fix xcode project
* Fix precondition catch
* Use switch syntax
* Add multiple choice element case
* Add explicit types in KeyedBox initialization
* Add explicitly empty parameter to KeyedBox initializer
* Use more concise type inference
* Unify switch syntax
* Cut down code duplication
* Fix formatting
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Arrays of enums Array of enums with associated values
4 participants